package pl.edu.mimuw.irs.core.model.analysis;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.model.analysis.dd.ActivityWrapper;
import pl.edu.mimuw.irs.core.model.analysis.resources.Fire;
import pl.edu.mimuw.irs.core.model.analysis.resources.Hire;
import pl.edu.mimuw.irs.core.model.analysis.resources.RequirementPlace;
import pl.edu.mimuw.irs.core.model.analysis.resources.ResourcePlace;
import pl.edu.mimuw.irs.core.model.design.resource.Resource;
import pl.edu.mimuw.irs.core.model.petrinet.Transition;
import pl.edu.mimuw.irs.core.util.Pair;

public class History implements Serializable {

	private static final long serialVersionUID = 1618388672446280976L;
	
	// *** Attributes

	private boolean dirty;
	private List<Transition> fullList;
	private LinkedHashMap<ActivityWrapper, Map<ResourcePlace, Set<RequirementPlace>>> busyActivities;
	private Map<ActivityWrapper, Time> startTimes;
	/*
	 * Chronology is created only for activities 
	 * (only activities supports timestamps). 
	 */
	private Map<Time, Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>> chronology;
	/*
	 * Recalculated chronology - organize events by order.
	 */
	private List<Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>> order;
	/* Total time taken */
	private Time timestamp;
	private Map<Resource, Time> usage;
	private Map<Resource, Time> workTime;
	public List<Transition> getFullList() {
		if (fullList == null) fullList = new LinkedList<Transition>();
		return fullList;
	}
	public Map<ActivityWrapper, Map<ResourcePlace, Set<RequirementPlace>>> getBusyActivities() {
		if (busyActivities == null) busyActivities =
			new LinkedHashMap<ActivityWrapper, Map<ResourcePlace, Set<RequirementPlace>>>();
		return busyActivities;
	}
	public Map<ActivityWrapper, Time> getStartTimes() {
		if (startTimes == null) startTimes = 
			new LinkedHashMap<ActivityWrapper, Time>();
		return startTimes;
	}
	public Map<Time, Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>> getChronology() {
		if (chronology == null) chronology = 
			new LinkedHashMap<Time, Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>>();
		return chronology;
	}
	
	/**
	 * @return ordered activity events (ordered chronology)
	 */
	public List<Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>> getOrder() {
		if (order == null || dirty == true) {
			List<Time> timestamps = new ArrayList<Time>(getChronology().keySet());
			Collections.sort(timestamps);
			/* Timestamps are sorted - we can build chronology with omitted timestamps. */
			order = new ArrayList<Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>>(timestamps.size());
			for (int i = 0; i < timestamps.size(); i++) {
				Time timestamp = timestamps.get(i);
				Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> element = getChronology().get(timestamp);
				order.add(i, element);
			}
			dirty = false;
		}
		return order;
	}
	public Time getTimestamp() {
		if (timestamp == null) timestamp = new Time();
		return timestamp;
	}
	public Map<Resource, Time> getUsage() {
		if (usage == null) usage = new LinkedHashMap<Resource, Time>();
		return usage;
	}
	public void addUsage(Resource resource, Time time) {
		Time usage = getUsage().get(resource);
		if (usage == null) {
			usage = new Time();
			getUsage().put(resource, usage);
		}
		usage.add(time);
		dirty = true;
	}
	public Map<Resource, Time> getWorkTime() {
		if (workTime == null) workTime = new LinkedHashMap<Resource, Time>();
		return workTime;
	}
	public void addWorkTime(Resource resource, Time time) {
		Time workTime = getWorkTime().get(resource);
		if (workTime == null) {
			workTime = new Time();
			getWorkTime().put(resource, workTime);
		}
		workTime.add(time);
		dirty = true;
	}

	// *** Constructors

	public History() {
		
	}
	
	// *** Methods

	/* 
	 * Automatic events adding 
	 */
	public void addInvoke(Transition transition) {
		if (transition instanceof Hire) {
			/*
			 * Getting and preparing parameters.
			 */
			Hire hire = (Hire) transition;
			ActivityWrapper activity = hire.getActivity();
			Map<ResourcePlace, Set<RequirementPlace>> hired = activity.getInvolvedResources();
			/* 
			 * If hire has not been performed because of lack of resources,
			 * we skip this step in the history.
			 */
			if (hired == null) return;
			Map<ResourcePlace, Set<RequirementPlace>> hiredCopy = 
				new LinkedHashMap<ResourcePlace, Set<RequirementPlace>>(hired);
			Time timestamp = activity.getTimestamp().copy();
			/*
			 * Storing data.
			 */
			getBusyActivities().put(activity, hiredCopy);
			getStartTimes().put(activity, timestamp);
			addEvent(timestamp, hire, hiredCopy);
		} else if (transition instanceof Fire) {
			/*
			 * Getting and preparing parameters.
			 */
			Fire fire = (Fire) transition;
			ActivityWrapper activity = fire.getActivity();
			Time timestamp = activity.getTimestamp().copy();
			Map<ResourcePlace, Set<RequirementPlace>> fired = getBusyActivities().get(activity);
			getBusyActivities().remove(activity);
			Time start = getStartTimes().get(activity);
			getStartTimes().remove(activity);
			/*
			 * Storing data.
			 */
			if (fired == null) {
				start.toString(); // Used for debug and throws NPE on hire-fire mismatch
			}
			for (ResourcePlace resourcePlace : fired.keySet()) {
				/*
				 * If resource works for some ancestry activity we still do not
				 * count its cost. We count it when it really finishes.
				 */
				if (activity.isDerived(resourcePlace)) continue;
				addUsage(resourcePlace.getResource(), new Time(start, timestamp));
				addWorkTime(resourcePlace.getResource(), activity.getTimeStrategy().getTimeTaken());
			}
			addEvent(timestamp, fire, fired);
			if (this.timestamp == null || this.timestamp.compareTo(timestamp) < 0) 
				this.timestamp = timestamp;
		}
		getFullList().add(transition);
		dirty = true;
	}
	
	/*
	 * Manual events adding.
	 */
	public void addEvent(Time timestamp, Transition event, Map<ResourcePlace, Set<RequirementPlace>> hiredCopy) {
		Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = getChronology().get(timestamp);
		if (events == null) {
			events = new LinkedHashMap<Transition, Map<ResourcePlace, Set<RequirementPlace>>>();
			getChronology().put(timestamp, events);
		}
		events.put(event, hiredCopy);
		dirty = true;
	}

	public int getTotalUsageCost() {
		int total = 0;
		for (Entry<Resource, Time> entry : getUsage().entrySet()) {
			total += entry.getKey().getUnitCost() * entry.getValue().getValue();
		}
		return total;
	}
	
	public List<Entry<Resource, Time>> getBusiest() {
		List<Entry<Resource, Time>> usage = 
			new ArrayList<Entry<Resource, Time>>(getUsage().entrySet());
		Collections.sort(usage, new BusiestComparator());
		return usage;
	}
	
	public List<Pair<Resource, Integer>> getMostExpensive() {
		List<Pair<Resource, Integer>> costs = 
			new ArrayList<Pair<Resource, Integer>>();
		for (Entry<Resource, Time> entry : getUsage().entrySet()) {
			costs.add(new Pair<Resource, Integer>(entry.getKey(), 
					entry.getValue().getValue() * entry.getKey().getUnitCost()));
		}
		Collections.sort(costs, new MostExpensiveComparator());
		return costs;
	}
	
	/* 
	 * Tells whether beginning of this history is described
	 * with provided constraints. 
	 */
	public boolean beginsWith(History constraints) {
		int size = constraints.getOrder().size();
		
		/*
		 * We must check if the same activities are performed during
		 * the same timestamps.
		 */
		if (getOrder().size() < size) return false;
		for (int i = 0; i < size; i++) {
			boolean last = (i == size - 1);
			/*
			 * If at current timestamp are some differences we return
			 * false
			 */
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> thisEvents = 
				getOrder().get(i);
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> constEvents = 
				constraints.getOrder().get(i);
			/* 
			 * If this is not the last timestamp all events must have
			 * occurred.
			 */
			if (!last && thisEvents.size() != constEvents.size()) 
				return false;
			/*
			 * The same count of events occurred or we are in the last step,
			 * but maybe these are not the same.
			 */
			for (Entry<Transition, Map<ResourcePlace, Set<RequirementPlace>>> entry : constEvents.entrySet()) {
				Map<ResourcePlace, Set<RequirementPlace>> thisHirings = thisEvents.get(entry.getKey());
				/* If any constraint event could not be found the histories differ. */
				if (thisHirings == null) return false;
				/* Check if the same resources were hired for this activity. */
				if (!last && thisHirings.size() != entry.getValue().size()) 
					return false;
				if (last && thisHirings.size() < entry.getValue().size()) 
					return false;
				/*
				 * It is important if resources are hired for the same
				 * requirements because it means they can be later used in 
				 * different situations.
				 */
				for (Entry<ResourcePlace, Set<RequirementPlace>> resourceEntry : 
						entry.getValue().entrySet()) {
					Set<RequirementPlace> requirements = thisHirings.get(resourceEntry.getKey());
					if (requirements == null) return false;
					if (!last && requirements.size() != resourceEntry.getValue().size()) 
						return false;
					if (last && requirements.size() < resourceEntry.getValue().size()) 
						return false;

					if (!requirements.containsAll(resourceEntry.getValue())) return false;
				}
			}
		}
		
		/*
		 * If we passed the for loop - the constraints history is the
		 * beginning of this history.
		 */
		return true;
	}
	
	/** Creates copy of this history, creating copies of all deeply contained maps/collections */ 
	public History copy() {
		History copy = new History();
		
		
		copy.fullList = new ArrayList<Transition>(fullList);
		copy.busyActivities = new LinkedHashMap<ActivityWrapper, Map<ResourcePlace, Set<RequirementPlace>>>();
		for (Entry<ActivityWrapper, Map<ResourcePlace, Set<RequirementPlace>>> entry : busyActivities.entrySet()) {
			Map<ResourcePlace, Set<RequirementPlace>> map = new LinkedHashMap<ResourcePlace, Set<RequirementPlace>>();
			copy.busyActivities.put(entry.getKey(), map);
			for (Entry<ResourcePlace, Set<RequirementPlace>> entry2 : entry.getValue().entrySet()) {
				Set<RequirementPlace> set = new LinkedHashSet<RequirementPlace>(entry2.getValue());
				map.put(entry2.getKey(), set);
			}
		}
		
		copy.startTimes = new LinkedHashMap<ActivityWrapper, Time>(startTimes);
		
		copy.chronology = new LinkedHashMap<Time, Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>>();
		for (Entry<Time, Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>> entry : chronology.entrySet()) {
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> map = 
				new LinkedHashMap<Transition, Map<ResourcePlace, Set<RequirementPlace>>>();
			copy.chronology.put(entry.getKey(), map);
			for (Entry<Transition, Map<ResourcePlace, Set<RequirementPlace>>> entry2 : entry.getValue().entrySet()) {
				Map<ResourcePlace, Set<RequirementPlace>> map2 = 
					new LinkedHashMap<ResourcePlace, Set<RequirementPlace>>();
				map.put(entry2.getKey(), map2);
				for (Entry<ResourcePlace, Set<RequirementPlace>> entry3 : entry2.getValue().entrySet()) {
					Set<RequirementPlace> set = new LinkedHashSet<RequirementPlace>(entry3.getValue());
					map2.put(entry3.getKey(), set);
				}
			}
		}
		
		copy.order = new ArrayList<Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>>>(order.size());
		for (Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> element : order) {
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> map = 
				new LinkedHashMap<Transition, Map<ResourcePlace, Set<RequirementPlace>>>();
			copy.order.add(map);
			for (Entry<Transition, Map<ResourcePlace, Set<RequirementPlace>>> entry : element.entrySet()) {
				Map<ResourcePlace, Set<RequirementPlace>> map2 = 
					new LinkedHashMap<ResourcePlace, Set<RequirementPlace>>();
				map.put(entry.getKey(), map2);
				for (Entry<ResourcePlace, Set<RequirementPlace>> entry2 : entry.getValue().entrySet()) {
					Set<RequirementPlace> set = new LinkedHashSet<RequirementPlace>(entry2.getValue());
					map2.put(entry2.getKey(), set);
				}
			}
		}
		
		copy.timestamp = timestamp.copy();
		copy.usage = new LinkedHashMap<Resource, Time>(usage);
		copy.workTime = new LinkedHashMap<Resource, Time>(workTime);
		
		return copy;
	}
	
	@Override
	public String toString() {
		return "History(" + getChronology() + ")";
	}
	
	public class BusiestComparator implements Comparator<Entry<Resource, Time>> {

		@Override
		public int compare(Entry<Resource, Time> arg0,
				Entry<Resource, Time> arg1) {
			return arg1.getValue().getValue() - arg0.getValue().getValue();
		}
	}

	public class MostExpensiveComparator implements Comparator<Pair<Resource, Integer>> {
		@Override
		public int compare(Pair<Resource, Integer> arg0,
				Pair<Resource, Integer> arg1) {
			return arg1.getValue() - arg0.getValue();
		}
	}
}
